package token

import (
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"strings"
	"time"

	"github.com/pkg/errors"

	jsoniter "github.com/json-iterator/go"
)

const (
	hashKey = "AnT8zQ1UeLsPrMc6"
)

type token struct {
	Token    interface{} `json:"token,omitempty"`
	ExpireAt int64       `json:"e,omitempty"`
}

type TokenCreator struct {
	hashKey string
	expired time.Time
}

func (t *TokenCreator) SetKey(key string) *TokenCreator {
	t.hashKey = key
	return t
}

func (t *TokenCreator) SetExpired(d time.Duration) *TokenCreator {
	t.expired = time.Now().Add(d)
	return t
}

func (t *TokenCreator) GenHashToken(v interface{}) (string, error) {
	tmpToken := token{Token: v}

	if !t.expired.IsZero() {
		tmpToken.ExpireAt = t.expired.Unix()
	}

	out, err := jsoniter.Marshal(tmpToken)
	if err != nil {
		return "", errors.Wrap(err, "token marshal err")
	}

	palin := fmt.Sprintf("%v%s", out, t.hashKey)
	hashInBytes := sha256.Sum256([]byte(palin))
	digest := hex.EncodeToString(hashInBytes[:])

	return fmt.Sprintf("%s.%s", base64.StdEncoding.EncodeToString(out), digest), nil
}

var (
	ErrTokenFmtInvalid     = errors.New("token format invalid")
	ErrTokenExpired        = errors.New("token expired")
	ErrTokenDigestNotMatch = errors.New("token digest not match")
)

func (t *TokenCreator) ValidHashToken(tokenStr string) error {
	tmpToken := token{}

	tokenArr := strings.Split(tokenStr, ".")
	if len(tokenArr) != 2 {
		return ErrTokenFmtInvalid
	}

	out, err := base64.StdEncoding.DecodeString(tokenArr[0])
	if err != nil {
		return ErrTokenFmtInvalid
	}

	if err := jsoniter.Unmarshal(out, &tmpToken); err != nil {
		return ErrTokenFmtInvalid
	}

	if tmpToken.ExpireAt > 0 && time.Now().Unix() > tmpToken.ExpireAt {
		return ErrTokenExpired
	}

	out, err = jsoniter.Marshal(tmpToken)
	if err != nil {
		return ErrTokenExpired
	}

	palin := fmt.Sprintf("%v%s", out, t.hashKey)
	hashInBytes := sha256.Sum256([]byte(palin))
	digest := hex.EncodeToString(hashInBytes[:])

	if digest != tokenArr[1] {
		return ErrTokenDigestNotMatch
	}
	return nil
}
